// Copyright (c) 2024 CHANGLEI. All rights reserved.

import 'dart:ui' as dart_ui;

import 'package:charts_grasp/src/chart/chart_series/series_renderer_properties.dart';
import 'package:charts_grasp/src/chart/chart_series/xy_data_series.dart';
import 'package:charts_grasp/src/chart/common/common.dart';
import 'package:charts_grasp/src/chart/common/data_label_renderer.dart';
import 'package:charts_grasp/src/chart/utils/helper.dart';
import 'package:charts_grasp/src/common/event_args.dart' show MarkerRenderArgs;
import 'package:flutter/material.dart';

export 'package:charts_grasp/src/core/core.dart' show DataMarkerType, TooltipAlignment;

@immutable
class MarkerSettings {
  const MarkerSettings({
    bool? isVisible,
    double? height = 8,
    double? width = 8,
    this.color,
    DataMarkerType? shape,
    double? borderWidth,
    this.borderColor,
    this.image,
  })  : isVisible = isVisible ?? false,
        height = height ?? 8,
        width = width ?? 8,
        shape = shape ?? DataMarkerType.circle,
        borderWidth = borderWidth ?? 2;

  final bool isVisible;

  final double height;

  final double width;

  final Color? color;

  final DataMarkerType shape;

  final Color? borderColor;

  final double borderWidth;

  final ImageProvider? image;

  @override
  bool operator ==(Object other) {
    if (identical(this, other)) {
      return true;
    }
    if (other.runtimeType != runtimeType) {
      return false;
    }

    return other is MarkerSettings &&
        other.isVisible == isVisible &&
        other.height == height &&
        other.width == width &&
        other.color == color &&
        other.shape == shape &&
        other.borderWidth == borderWidth &&
        other.borderColor == borderColor &&
        other.image == image;
  }

  @override
  int get hashCode {
    final List<Object?> values = <Object?>[isVisible, height, width, color, shape, borderWidth, borderColor, image];
    return Object.hashAll(values);
  }
}

class MarkerDetails {
  const MarkerDetails({this.markerType, this.color, this.borderColor, this.borderWidth, this.size});

  final DataMarkerType? markerType;

  final Color? color;

  final Color? borderColor;

  final double? borderWidth;

  final Size? size;
}

class MarkerSettingsRenderer {
  MarkerSettingsRenderer(this.markerSettings) {
    color = markerSettings.color;

    borderColor = markerSettings.borderColor;

    borderWidth = markerSettings.borderWidth;
  }

  final MarkerSettings markerSettings;

  Color? color;

  Color? borderColor;

  late double borderWidth;

  dart_ui.Image? image;

  bool isImageDrawn = false;

  void renderMarker(SeriesRendererDetails seriesRendererDetails, CartesianChartPoint<dynamic> point,
      Animation<double>? animationController, Canvas canvas, int markerIndex,
      [int? outlierIndex,]) {
    final List<CartesianChartPoint<dynamic>> dataPoints = getSampledData(seriesRendererDetails);
    final bool isDataPointVisible = isLabelWithinRange(seriesRendererDetails, dataPoints[markerIndex]);
    Paint strokePaint;
    Paint fillPaint;
    final XyDataSeries<dynamic, dynamic> series = seriesRendererDetails.series as XyDataSeries<dynamic, dynamic>;
    Size size = Size(series.markerSettings.width, series.markerSettings.height);
    CartesianChartPoint<dynamic> point;
    DataMarkerType markerType = series.markerSettings.shape;
    final seriesColor = seriesRendererDetails.seriesColor;
    point = dataPoints[markerIndex];
    MarkerRenderArgs? event;
    borderColor = series.markerSettings.borderColor ?? seriesColor;
    color = series.markerSettings.color;
    borderWidth = series.markerSettings.borderWidth;
    final bool isMarkerEventTriggered = CartesianPointHelper.getIsMarkerEventTriggered(point);
    if (isDataPointVisible &&
        seriesRendererDetails.chart.onMarkerRender != null &&
        seriesRendererDetails.isMarkerRenderEvent == false) {
      if (animationController == null ||
          ((animationController.value == 0.0 &&
                  !isMarkerEventTriggered &&
                  animationController.status == AnimationStatus.forward) ||
              (seriesRendererDetails.animationController.duration!.inMilliseconds == 0 && !isMarkerEventTriggered))) {
        CartesianPointHelper.setIsMarkerEventTriggered(point, true);
        event = triggerMarkerRenderEvent(
            // ignore: unnecessary_null_checks
            seriesRendererDetails, size, markerType, dataPoints[markerIndex].visiblePointIndex!, animationController,)!;
        markerType = event.shape;
        borderColor = event.borderColor;
        color = event.color;
        borderWidth = event.borderWidth;
        size = Size(event.markerHeight, event.markerWidth);
        CartesianPointHelper.setMarkerDetails(
            point,
            MarkerDetails(
                markerType: markerType, borderColor: borderColor, color: color, borderWidth: borderWidth, size: size,),);
      }
    }
    final bool hasPointColor = series.pointColorMapper != null;
    final bool isBoxSeries = seriesRendererDetails.seriesType.contains('boxandwhisker');
    final double opacity = (animationController != null &&
            (seriesRendererDetails.stateProperties.renderingDetails.initialRender! == true ||
                seriesRendererDetails.needAnimateSeriesElements == true))
        ? animationController.value
        : 1;
    final MarkerDetails? pointMarkerDetails = CartesianPointHelper.getMarkerDetails(point);
    !isBoxSeries
        ? seriesRendererDetails.markerShapes.add(isDataPointVisible
            ? getMarkerShapesPath(
                pointMarkerDetails?.markerType ?? markerType,
                Offset(point.markerPoint!.x, point.markerPoint!.y),
                pointMarkerDetails?.size ?? size,
                seriesRendererDetails,
                markerIndex,
                null,
                animationController,)
            : null,)
        : seriesRendererDetails.markerShapes.add(isDataPointVisible
            ? getMarkerShapesPath(
                pointMarkerDetails?.markerType ?? markerType,
                Offset(point.outliersPoint[outlierIndex!].x, point.outliersPoint[outlierIndex].y),
                pointMarkerDetails?.size ?? size,
                seriesRendererDetails,
                markerIndex,
                null,
                animationController,)
            : null,);
    if (seriesRendererDetails.seriesType.contains('range') == true) {
      seriesRendererDetails.markerShapes2.add(isDataPointVisible
          ? getMarkerShapesPath(
              pointMarkerDetails?.markerType ?? markerType,
              Offset(point.markerPoint2!.x, point.markerPoint2!.y),
              pointMarkerDetails?.size ?? size,
              seriesRendererDetails,
              markerIndex,
              null,
              animationController,)
          : null,);
    }
    strokePaint = getStrokePaint(point, series, pointMarkerDetails, opacity, hasPointColor, seriesColor, markerType,
        seriesRendererDetails, animationController, size,);

    fillPaint = getFillPaint(point, series, seriesRendererDetails, pointMarkerDetails, opacity);
    final bool isScatter = seriesRendererDetails.seriesType == 'scatter';
    final Rect axisClipRect = seriesRendererDetails.stateProperties.chartAxis.axisClipRect;

    if ((series.markerSettings.isVisible || isScatter || isBoxSeries) &&
        point.isVisible &&
        withInRect(seriesRendererDetails, point.markerPoint, axisClipRect) &&
        point.isGap != true &&
        (!isScatter || series.markerSettings.shape == DataMarkerType.image) &&
        seriesRendererDetails.markerShapes[isBoxSeries ? outlierIndex! : markerIndex] != null) {
      seriesRendererDetails.renderer.drawDataMarker(
          isBoxSeries ? outlierIndex! : markerIndex,
          canvas,
          fillPaint,
          strokePaint,
          isBoxSeries ? point.outliersPoint[outlierIndex!].x : point.markerPoint!.x,
          isBoxSeries ? point.outliersPoint[outlierIndex!].y : point.markerPoint!.y,
          seriesRendererDetails.renderer,);
      if (series.markerSettings.shape == DataMarkerType.image) {
        drawImageMarker(
            seriesRendererDetails,
            canvas,
            isBoxSeries ? point.outliersPoint[outlierIndex!].x : point.markerPoint!.x,
            isBoxSeries ? point.outliersPoint[outlierIndex!].y : point.markerPoint!.y,);
        if (seriesRendererDetails.seriesType.contains('range') == true || seriesRendererDetails.seriesType == 'hilo') {
          drawImageMarker(seriesRendererDetails, canvas, point.markerPoint2!.x, point.markerPoint2!.y);
        }
      }
    }
    setMarkerEventTrigged(point, seriesRendererDetails, animationController);
  }

  void setMarkerEventTrigged(
    CartesianChartPoint<dynamic> point,
    SeriesRendererDetails seriesRendererDetails,
    Animation<double>? animationController,
  ) {
    if (seriesRendererDetails.chart.onMarkerRender != null && seriesRendererDetails.isMarkerRenderEvent == false) {
      if (animationController == null ||
          ((animationController.value == 1.0 && animationController.status == AnimationStatus.completed) ||
              (seriesRendererDetails.animationController.duration!.inMilliseconds == 0))) {
        CartesianPointHelper.setIsMarkerEventTriggered(point, false);
      }
    }
  }

  Paint getFillPaint(CartesianChartPoint<dynamic> point, XyDataSeries<dynamic, dynamic> series,
      SeriesRendererDetails seriesRendererDetails, MarkerDetails? pointMarkerDetails, double opacity,) {
    return Paint()
      ..color = (point.isEmpty ?? false)
          ? series.emptyPointSettings.color
          : color != Colors.transparent
              ? (pointMarkerDetails?.color ??
                      color ??
                      (seriesRendererDetails.stateProperties.renderingDetails.chartTheme.brightness == Brightness.light
                          ? Colors.white
                          : Colors.black))
                  .withOpacity(opacity)
              : color!
      ..style = PaintingStyle.fill;
  }

  Paint getStrokePaint(
      CartesianChartPoint<dynamic> point,
      XyDataSeries<dynamic, dynamic> series,
      MarkerDetails? pointMarkerDetails,
      double opacity,
      bool hasPointColor,
      Color? seriesColor,
      DataMarkerType markerType,
      SeriesRendererDetails seriesRendererDetails,
      Animation<double>? animationController,
      Size size,) {
    Paint strokePaint = Paint()
      ..color = (point.isEmpty ?? false)
          ? (series.emptyPointSettings.borderWidth == 0
              ? Colors.transparent
              : series.emptyPointSettings.borderColor.withOpacity(opacity))
          : (series.markerSettings.borderWidth == 0
              ? Colors.transparent
              : (pointMarkerDetails?.borderColor != null
                  ? pointMarkerDetails!.borderColor!.withOpacity(opacity)
                  : (hasPointColor && point.pointColorMapper != null)
                      ? point.pointColorMapper!.withOpacity(opacity)
                      : (borderColor != null ? borderColor!.withOpacity(opacity) : seriesColor!.withOpacity(opacity))))
      ..style = PaintingStyle.stroke
      ..strokeWidth = (point.isEmpty ?? false)
          ? series.emptyPointSettings.borderWidth
          : pointMarkerDetails?.borderWidth ?? borderWidth;

    if (series.gradient != null &&
        series.markerSettings.borderColor == null &&
        ((pointMarkerDetails == null) || (pointMarkerDetails.borderColor == null))) {
      strokePaint = getLinearGradientPaint(
          series.gradient!,
          getMarkerShapesPath(
                  pointMarkerDetails?.markerType ?? markerType,
                  Offset(point.markerPoint!.x, point.markerPoint!.y),
                  pointMarkerDetails?.size ?? size,
                  seriesRendererDetails,
                  null,
                  null,
                  animationController,)
              .getBounds(),
          seriesRendererDetails.stateProperties.requireInvertedAxis,);
      strokePaint.style = PaintingStyle.stroke;
      strokePaint.strokeWidth = (point.isEmpty ?? false)
          ? series.emptyPointSettings.borderWidth
          : pointMarkerDetails?.borderWidth ?? series.markerSettings.borderWidth;
    }

    return strokePaint;
  }

  bool withInRect(SeriesRendererDetails seriesRendererDetails, ChartLocation? markerPoint, Rect axisClipRect) {
    bool withInRect = false;

    withInRect = markerPoint != null &&
        markerPoint.x >= axisClipRect.left &&
        markerPoint.x <= axisClipRect.right &&
        markerPoint.y <= axisClipRect.bottom &&
        markerPoint.y >= axisClipRect.top;

    return withInRect;
  }

  void drawImageMarker(SeriesRendererDetails seriesRendererDetails, Canvas canvas, double pointX, double pointY) {
    if (seriesRendererDetails.markerSettingsRenderer!.image != null) {
      final double imageWidth = 2 * seriesRendererDetails.series.markerSettings.width;
      final double imageHeight = 2 * seriesRendererDetails.series.markerSettings.height;
      final Rect positionRect =
          Rect.fromLTWH(pointX - imageWidth / 2, pointY - imageHeight / 2, imageWidth, imageHeight);
      paintImage(canvas: canvas, rect: positionRect, image: image!, fit: BoxFit.fill);
    }
  }
}
